using Cofoundry.Core.Data;
using Cofoundry.Domain.Data;
using Cofoundry.Domain.Data.Internal;

namespace Cofoundry.Domain.Internal;

/// <summary>
/// Publishes a custom entity. If the custom entity is already published and
/// a date is specified then the publish date will be updated.
/// </summary>
public class PublishCustomEntityCommandHandler
    : ICommandHandler<PublishCustomEntityCommand>
    , IIgnorePermissionCheckHandler
{
    private readonly CofoundryDbContext _dbContext;
    private readonly ICommandExecutor _commandExecutor;
    private readonly IQueryExecutor _queryExecutor;
    private readonly ICustomEntityCache _customEntityCache;
    private readonly IMessageAggregator _messageAggregator;
    private readonly IPermissionValidationService _permissionValidationService;
    private readonly ICustomEntityDefinitionRepository _customEntityDefinitionRepository;
    private readonly ITransactionScopeManager _transactionScopeManager;
    private readonly ICustomEntityStoredProcedures _customEntityStoredProcedures;

    public PublishCustomEntityCommandHandler(
        CofoundryDbContext dbContext,
        ICommandExecutor commandExecutor,
        IQueryExecutor queryExecutor,
        ICustomEntityCache customEntityCache,
        IMessageAggregator messageAggregator,
        IPermissionValidationService permissionValidationService,
        ICustomEntityDefinitionRepository customEntityDefinitionRepository,
        ITransactionScopeManager transactionScopeManager,
        ICustomEntityStoredProcedures customEntityStoredProcedures
        )
    {
        _dbContext = dbContext;
        _queryExecutor = queryExecutor;
        _commandExecutor = commandExecutor;
        _customEntityCache = customEntityCache;
        _messageAggregator = messageAggregator;
        _permissionValidationService = permissionValidationService;
        _customEntityDefinitionRepository = customEntityDefinitionRepository;
        _transactionScopeManager = transactionScopeManager;
        _customEntityStoredProcedures = customEntityStoredProcedures;
    }

    public async Task ExecuteAsync(PublishCustomEntityCommand command, IExecutionContext executionContext)
    {
        // Prefer draft, but update published entity if no draft (only one draft permitted)
        var version = await _dbContext
            .CustomEntityVersions
            .Include(v => v.CustomEntity)
            .Where(v => v.CustomEntityId == command.CustomEntityId && (v.WorkFlowStatusId == (int)WorkFlowStatus.Draft || v.WorkFlowStatusId == (int)WorkFlowStatus.Published))
            .OrderByDescending(v => v.WorkFlowStatusId == (int)WorkFlowStatus.Draft)
            .ThenByDescending(v => v.CreateDate)
            .FirstOrDefaultAsync();

        EntityNotFoundException.ThrowIfNull(version, command.CustomEntityId);

        var definition = _customEntityDefinitionRepository.GetRequiredByCode(version.CustomEntity.CustomEntityDefinitionCode);
        _permissionValidationService.EnforceCustomEntityPermission<CustomEntityPublishPermission>(definition.CustomEntityDefinitionCode, executionContext.UserContext);

        var hasEntityPublishStatusChanged = version.CustomEntity.SetPublished(executionContext.ExecutionDate, command.PublishDate);

        if (version.WorkFlowStatusId == (int)WorkFlowStatus.Published && !hasEntityPublishStatusChanged)
        {
            // only thing we can do with a published version is update the date
            await _dbContext.SaveChangesAsync();
            await _transactionScopeManager.QueueCompletionTaskAsync(_dbContext, () => OnTransactionComplete(version));
        }
        else
        {
            await ValidateTitleAsync(version, definition, executionContext);

            using (var scope = _transactionScopeManager.Create(_dbContext))
            {
                await UpdateUrlSlugIfRequiredAsync(version, definition, executionContext);
                version.WorkFlowStatusId = (int)WorkFlowStatus.Published;

                await _dbContext.SaveChangesAsync();
                await _customEntityStoredProcedures.UpdatePublishStatusQueryLookupAsync(command.CustomEntityId);

                scope.QueueCompletionTask(() => OnTransactionComplete(version));
                await scope.CompleteAsync();
            }
        }
    }

    private Task OnTransactionComplete(CustomEntityVersion version)
    {
        _customEntityCache.Clear(version.CustomEntity.CustomEntityDefinitionCode, version.CustomEntityId);

        return _messageAggregator.PublishAsync(new CustomEntityPublishedMessage()
        {
            CustomEntityId = version.CustomEntityId,
            CustomEntityDefinitionCode = version.CustomEntity.CustomEntityDefinitionCode
        });
    }

    /// <summary>
    /// If the url slug is autogenerated, we need to update it only when the custom entity is published.
    /// </summary>
    private async Task UpdateUrlSlugIfRequiredAsync(CustomEntityVersion dbVersion, ICustomEntityDefinition definition, IExecutionContext executionContext)
    {
        if (!definition.AutoGenerateUrlSlug)
        {
            return;
        }

        var slug = SlugFormatter.ToSlug(dbVersion.Title);

        if (slug == dbVersion.CustomEntity.UrlSlug)
        {
            return;
        }

        var urlCommand = new UpdateCustomEntityUrlCommand()
        {
            CustomEntityId = dbVersion.CustomEntityId,
            LocaleId = dbVersion.CustomEntity.LocaleId,
            UrlSlug = slug
        };

        await _commandExecutor.ExecuteAsync(urlCommand, executionContext);
    }

    private async Task ValidateTitleAsync(CustomEntityVersion dbVersion, ICustomEntityDefinition definition, IExecutionContext executionContext)
    {
        if (!definition.ForceUrlSlugUniqueness || SlugFormatter.ToSlug(dbVersion.Title) == dbVersion.CustomEntity.UrlSlug)
        {
            return;
        }

        var query = GetUniquenessQuery(dbVersion, definition);
        var isUnique = await _queryExecutor.ExecuteAsync(query, executionContext);

        if (!isUnique)
        {
            var titleTerm = definition.GetTerms().GetValueOrDefault(CustomizableCustomEntityTermKeys.Title, "title").ToLower();
            var message = $"Cannot publish because the {titleTerm} '{dbVersion.Title}' is not unique (symbols and spaces are ignored in the uniqueness check)";

            throw new UniqueConstraintViolationException(message, "Title", dbVersion.Title);
        }
    }

    private static IsCustomEntityUrlSlugUniqueQuery GetUniquenessQuery(CustomEntityVersion dbVersion, ICustomEntityDefinition definition)
    {
        var query = new IsCustomEntityUrlSlugUniqueQuery();
        query.CustomEntityDefinitionCode = definition.CustomEntityDefinitionCode;
        query.LocaleId = dbVersion.CustomEntity.LocaleId;
        query.UrlSlug = SlugFormatter.ToSlug(dbVersion.Title);
        query.CustomEntityId = dbVersion.CustomEntityId;

        return query;
    }
}
